Virtual DOM 中那些你不知道的事

虚拟DOM是啥?以及diff算法原理

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能
原理:

  • 把树形结构按照层级分解,只比较同级元素
  • 给列表结构的每个单元添加唯一的 key 属性,方便比较
  • React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
  • 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
  • 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能

diff算法,DOM树对比过程

https://github.com/lulujianglab/blog/issues/46

为什么虚拟 dom 会提高性能?

  • DOM是浏览器中的概念,用js对象表示页面上的元素,并提供操作DOM对象的API
  • 虚拟DOM就是一个JS对象(数据+JXS模板),用一个js对象来描述真实的DOM

虚拟DOM提高性能,不是说不操作DOM,而是减少操作DOM的次数,减少回流和重绘
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能

  • 用 JavaScript 对象结构表示 DOM 树的结构;
  • 然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 当状态变更的时候,重新构造一棵新的对象树。
  • 然后用新的树和旧的树进行比较,记录两棵树差异
  • 把记录的差异应用到真正的 DOM 树上,视图就更新了

使用diff算法比较新旧虚拟DOM----即比较两个js对象不怎么耗性能,而比较两个真实的DOM比较耗性能,从而虚拟DOM极大的提升了性能

虚拟DOM的目的?

实现页面中DOM元素的高效更新

虚拟DOM会比真实DOM快吗?什么情况下用虚拟DOM好

虚拟DOM并不一定比原生操作DOM快。需不需要虚拟DOM,其实与框架的DOM操作机制有关
React 的基本思维模式是每次有变动就重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。
比如,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。

可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起patch简化的dom操作省下来的时间可观的多。
可以看到,innerHTML 的总计算量是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。和 DOM 操作比起来,js 计算是极其快速的。
这才是为什么要有 Virtual DOM:它保证了
1)不管你的数据变化多少,每次重绘的性能都可以接受;
2) 你依然可以用类似 innerHTML 的思路去写你的应用。

这也是 React 厉害的地方。并不是说它比 DOM 快,而是说不管你数据怎么变化,我都可以以最小的代价来进行更新 DOM。 方法就是我在内存里面用新的数据重新生成一个虚拟 DOM 树,然后比较新旧 DOM,找出差异,再更新到 DOM 树上。这就是所谓的 diff 算法

  • 虚拟DOM减少了真实DOM的操作,当修改数据的时候,就是修改虚拟DOM产生全新的虚拟DOM。新旧虚拟DOM使用diff算法,得到patch(也就是需要修改的部分),然后将这个patch打到浏览器的DOM上(减少重绘和回流,从而达到性能优化的目的)
  • 每次DOM操作会引起重绘或者回流,频繁的真实DOM的修改会触发多次的排版和重绘相当耗性能。
    总之,一切为了减弱频繁的大面积重绘引发的性能问题,不同框架不一定需要虚拟DOM,关键看框架是否频繁会引发大面积的DOM操作

react是怎么工作的,怎么提高性能

主要还是说了下react的生命周期,还有shouldComponentUpdate这个函数,以及diff算法
https://github.com/lulujianglab/blog/issues/47

React 的工作原理

React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 "diffing" 算法来标记虚拟 DOM 中的改变,第二步是调节(reconciliation),会用 diff 的结果来更新 DOM

react 事件绑定

由于类的方法默认不会绑定this,因此在调用的时候如果忘记绑定,this的值将会是undefined。
绑定方式有以下几种:

  • 在构造函数中使用bind绑定this
    constructor(props) {
      super(props)
      this.handleClick = this.handleClick.bind(this)
    }
    
  • 在调用的时候使用bind绑定this
    <button onClick={this.handleClick.bind(this)}>
    
  • 在调用的时候使用箭头函数绑定this
    <button onClick={()=>this.handleClick()}>
    
  • 使用属性初始化语法绑定this(实验性)
      handleClick=()=>{
        console.log('this is:', this);
      }
      <button onClick={this.handleClick}>
    

比较:

  • 方式2和方式3都是在调用的时候再绑定this

    • 写法比较简单,当组件中没有state的时候就不需要添加类构造函数来绑定this
    • 每一次调用的时候都会生成一个新的方法实例,因此对性能有影响,并且当这个函数作为属性值传入低阶组件的时候,这些组件可能会进行额外的重新渲染,因为每一次都是新的方法实例作为的新的属性传递。
  • 方式1在类构造函数中绑定this,调用的时候不需要再绑定

    • 优点:只会生成一个方法实例,并且绑定一次之后如果多次用到这个方法也不需要再绑定
    • 缺点:即使不用到state,也需要添加类构造函数来绑定this,代码量多一点
  • 利用属性初始化语法(箭头函数声明),将方法初始化为箭头函数,因此在创建函数的时候就绑定了this(只会生成一个)

    • 优点:创建方法就绑定this,不需要在类构造函数中绑定,调用的时候不需要再作绑定。结合了方式1、方式2、方式3的优点
    • 需要用babel转译,且不能带参数,要不然还得使用方式二和三

方式1是官方推荐的绑定方式,也是性能最好的方式。方式2和方式3会有性能影响并且当方法作为属性传递给子组件的时候会引起重渲问题。方式4目前是最好的绑定方式,需要结合bable转译
this 的本质就是:this跟作用域无关的,只跟执行上下文有关

注意:只要是需要在调用的地方传参,就必须在事件绑定的地方使用bind或者箭头函数.又回到了方式二和方式三

生命周期

  • 初始化
    • 类的构造方法constructor():初始化props、state
  • 挂载
    • componentWillMount():组件即将被装载、渲染到页面上,整个生命周期中只会调用一次(在这里请求异步数据,render可能不会渲染到,因为componentWillMount执行后,render立马执行)
    • render():创建虚拟dom,进行diff算法,更新dom树
    • componentDidMount(): 组件被挂载到页面(渲染到 DOM 中)之后调用,整个生命周期只调用一次(可以异步请求数据,不建议使用setState函数,会触发额外的渲染,导致性能问题)
  • 更新
    • componentWillReceiveProps(nextprops):组件从父组件中接受了新的props(初始化时不调用。可以调用setState来更新组件状态,旧的属性可以通过this.props获取)
    • shouldComponentUpdate(nextprops,nextstate):组件接受到新属性或者新状态(组件接受新的props或state。返回布尔值,为true才更新组件。通过对比新旧数据避免生成新的dom树和旧的进行diff算法对比,从而优化性能。因为父组件render()调用会使得子组件render()也被执行)
    • componentWillUpdate(nextprops,nextstate): 组件更新之前(componentshouldupdate返回true)时调用,组件初始化时不调用(不能调用setState,会导致死循环)
    • render():创建虚拟dom,进行diff算法,更新dom树
    • componentDidUpdate():组件更新完成之后调用,组件初始化时候不调用(可以在这里获取doms)
  • 卸载
    • componentWillUnmount(): 组件即将被卸载时执行(在这里清除一些不需要的监听和计时器)

函数式编程,纯函数

React创建组件的方式

React 中有三种构建组件的方式
React.createClass()、ES6 class 和无状态函数

组件性能优化

shouldComponentUpdate(react 性能优化是哪个周期函数?)

这个方法用来判断是否需要调用 render 方法重新描绘 dom。
因为 dom 的描绘非常消耗性能,如果我们能在 shouldComponentUpdate 方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

pureComponent

不可变数据

key

等等优化方法,每一点的优点和缺点

如何设计一个好组件?容器组件和展示组件

合理划分组件,分为业务组件和技术组件

  • 根据组件的职责通常把组件分为 UI 组件和容器组件
  • UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑
  • 两者通过 React-Redux 提供 connect 方法联系起来

调用setState之后发生了什么

  • 首先,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程。(对比新的virtual dom树以及旧的virtual dom树,接着找出两者所不同的地方,根据不同的地方来修改现有的DOM)
  • 调和(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。为此,React将构建一个新的 React 元素树。(UI的对象表示)
  • 在得到元素树之后,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较。自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染
  • 在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染

refs的作用

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。
我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

class CustomForm extends Component {
  handleSubmit = () => {
    console.log("Input Value: ", this.input.value)
  }
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type='text'
          ref={(input) => this.input = input} />
        <button type='submit'>Submit</button>
      </form>
    )
  }
}

上述代码中的 input 域包含了一个 ref 属性,该属性声明的回调函数会接收 input 对应的 DOM 元素,我们将其绑定到 this 指针以便在其他的类函数中使用。
另外值得一提的是,refs 并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:

function CustomForm ({handleSubmit}) {
  let inputElement
  return (
    <form onSubmit={() => handleSubmit(inputElement.value)}>
      <input
        type='text'
        ref={(input) => inputElement = input} />
      <button type='submit'>Submit</button>
    </form>
  )
}

如果你创建了类似于下面的 Twitter 元素,那么它相关的类定义是啥样子的?

<Twitter username='tylermcginnis33'>
  {(user) => user === null
    ? <Loading />
    : <Badge info={user} />}
</Twitter>

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
// fetchUser take in a username returns a promise
// which will resolve with that username's data.
class Twitter extends Component {
  // finish this
}

回调渲染模式:这种模式中,组件会接收某个函数作为其子组件,然后在渲染函数中以 props.children 进行调用

这种模式的优势在于将父组件与子组件解耦和,父组件可以直接访问子组件的内部状态而不需要再通过 Props 传递,这样父组件能够更为方便地控制子组件展示的 UI 界面

譬如产品经理让我们将原本展示的 Badge 替换为 Profile,我们可以轻易地修改下回调函数

import React, { Component, PropTypes } from 'react'
import fetchUser from 'twitter'
class Twitter extends Component {
  state = {
    user: null,
  }
  static propTypes = {
    username: PropTypes.string.isRequired,
  }
  componentDidMount () {
    fetchUser(this.props.username)
      .then((user) => this.setState({user}))
  }
  render () {
    return this.props.children(this.state.user)
  }
}

何为 Children

  • 在JSX表达式中,一个开始标签(比如)和一个关闭标签(比如)之间的内容会作为一个特殊的属性props.children被自动传递给包含着它的组件
  • 这个属性有许多可用的方法,包括 React.Children.map,React.Children.forEach, React.Children.count, React.Children.only,React.Children.toArray

展示组件(Presentational component)和容器组件(Container component)之间有何不同

  • 展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态
  • 容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Redux actions,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源

类组件(Class component)和函数式组件(Functional component)之间有何不同

  • 类组件不仅允许你使用更多额外的功能,如组件自身的状态和生命周期钩子,也能使组件直接访问 store 并维持状态
  • 当组件仅是接收 props,并将组件自身渲染到页面时,该组件就是一个 '无状态组件(stateless component)',可以使用一个纯函数来创建这样的组件。这种组件也被称为哑组件(dumb components)或展示组件

(组件的)状态(state)和属性(props)之间有何不同

  • State 是一种数据结构,用于组件挂载时所需数据的默认值。State 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果.它是私有的
  • Props(properties 的简写)则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的(immutable)。组件不能改变自身的 props,但是可以把其子组件的 props 放在一起(统一管理)。Props 也不仅仅是数据--回调函数也可以通过 props 传递

何为受控组件(controlled component)

  • 在 HTML 中,类似 <input>, <textarea><select> 这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。
  • 但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange 会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为"受控元素"。
handleChangeValue = (e) => {
  this.setState({
    inputValue: e.target.value
  })
}

<Input 
  placeholder="请输入"
  value={inputValue.trim()}
  onChange={this.handleChangeValue}
  onPressEnter={this.handleSearch}
/>

高阶组件是什么和常见的高阶组件

高阶组件是一个以组件为参数并返回一个新组件的函数。
HOC 运行你重用代码、逻辑和引导抽象。
最常见的可能是 Redux 的 connect 函数和antD的Form.create()组件。
除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果你发现你在不同的地方写了大量代码来做同一件事时,就应该考虑将代码重构为可重用的 HOC

为什么建议传递给 setState 的参数是一个 callback 而不是一个对象

因为 this.props 和 this.state 的更新可能是异步的,不能依赖它们的值去计算下一个 state。而通过callback的第一个参数可以拿到上一次的prevState,此时的prevState也是合并了前面多次setState计算的结果

setState第二个参数支持回调函数,在回调里state是最新的。并且回调的执行时机在于state合并处理之后

怎么立即获取到修改后的state呢?可以通过 setState 中传递函数的方式及回调去实现

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 2
  })
}

如果只是通过回调去实现,只能立即获取上一步修改后的结果

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 1
  })
}

demo-01:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 四:2
  })
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 五:2
  })
  console.log("console-end: " + this.state.count) // 二:0
}

demo-02:

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 一:0
  this.setState(prevState => {
    console.log("console from func: " + prevState.count); // 三:1
    return {
      count: prevState.count + 1
    };
  }, ()=>{
    console.log('last console: '+ this.state.count) // 四:1
  })
  this.setState({ count: this.state.count + 1 }, () => {
    console.log("console from callback: " + this.state.count); // 五:1
  })
  console.log("console-end: " + this.state.count) // 二:0
}

React 其实会维护着一个 state 的更新队列,每次调用 setState 都会先把当前修改的 state 推进这个队列,在最后,React 会对这个队列进行合并处理,然后去执行回调。根据最终的合并结果再去走下面的流程(更新虚拟dom,触发渲染)

setState为什么要设计成异步的?react的setState同步还是异步?

  • 保证内部的一致性:即使state是同步更新,props也不是。(你只有在父组件重新渲染时才能知道props)
  • 将state的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次的状态改变都去重新渲染真实dom,那么它将带来巨大的性能消耗

setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套

state = {
  count: 0
}

componentDidMount(){
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log("console: " + this.state.count) // 0
  setTimeout(() => { // setTimeout中调用
    console.log("setTimeout-first: " + this.state.count); // 1
    this.setState({ count: this.state.count + 1 });
    console.log("setTimeout-end: " + this.state.count); // 2
  }, 0)
}

(在构造函数中)调用 super(props) 的目的是什么

在 super() 被调用之前,子类是不能使用 this 的,在 ES6 中,子类必须在 constructor 中调用 super()。
传递 props 给 super() 的原因则是便于(在子类中)能在 constructor 访问 this.props

如何实现异步网络请求的?应该在 React 组件的何处发起 Ajax 请求,为什么

在 React 组件中,应该在 componentDidMount 中发起网络请求。
这个方法会在组件第一次“挂载”(被添加到 DOM)时执行,在组件的生命周期中仅会执行一次。
如果在之前发起请求,可能在组件挂载之前 Ajax 请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState,将不起作用。在 componentDidMount 中发起网络请求将保证这有一个组件可以更新了。

react16新特性

尤其理解time slice和suspense

在 React 当中 Element 和 Component 有何区别

简单地说,一个 React element 描述了你想在屏幕上看到什么。换个说法就是,一个 React element 是一些 UI 的对象表示。

一个 React Component 是一个函数或一个类,它可以接受输入并返回一个 React element t(通常是通过 JSX ,它被转化成一个 createElement 调用)

路由实现原理

react-router等一众路由插件实现的功能是更新页面的视图,但是却不重新请求页面,也就是说,其实,他们并没有实际进行了跳转,而是修改了页面的DOM并通过修改页面的URL来模拟跳转
在HTML5之前,页面路由只有hash模式,而HTML5中History对象的新增方法,带来了另一种模式:history模式

hash模式:
在HTML5之前,vue-router是通过修改URL的hash值来达到修改页面URL并生成历史记录,虽然不会重新请求页面,但会发现路径前总会有一个#,比如http://localhost:8080/#/b
因为没开启history模式的情况下,vue-router是通过hashchange事件来监听URL中hash的改变并通过修改hash来模拟路径的变化
hash模式最大的优点是兼容性强,可以兼容一众老式浏览器。而它最大的缺点是,页面URL中一直挂着一个难看的#

history模式:
HTML5发布后,又有了history模式
vue-router的history模式就是通过HTML5中History对象的pushState方法进行模拟的
HTML5还提供了一个popstate事件,当用户点击前进、后退按钮,或者调用back、forward、go方法时触发,可以监听URL的改变
不仅如此,还可以使用pushState的第一个参数进行传值,使用History的state属性进行取值。
但是目前只有兼容了HTML5的浏览器(IE10+)才能使用history模式

简述 flux 思想

Flux 的最大特点,就是数据的"单向流动"。

  • 用户访问 View
  • View 发出用户的 Action
  • Dispatcher 收到 Action,要求 Store 进行相应的更新
  • Store 更新后,发出一个"change"事件
  • View 收到"change"事件后,更新页面

React 项目用过什么脚手架

  • creat-react-app
  • umijs

redux 有什么缺点

  • 一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取。
  • 当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的 shouldComponentUpdate 进行判断

组件间通信

React key是干嘛的?

Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识

  • 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性
  • 在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染
  • 此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系

说一下 redux ,redux、react-redux等原理

https://github.com/lulujianglab/blog/issues/34

  • redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理
  • 主要有三个核心方法,action,store,reducer

基本思想

整个应用的 state 保持在一个单一的 store 中
改变应用 state 的唯一方式是在应用中触发 actions,然后为这些 actions 编写 reducers 来修改 state

store

Store 是一个 javascript 对象,它保存了整个应用的 state。与此同时,Store 也承担以下职责:

  • 允许通过 getState() 访问 state
  • 运行通过 dispatch(action) 改变 state
  • 通过 subscribe(listener) 注册 listeners
  • 通过 subscribe(listener) 返回的函数处理 listeners 的注销

action

Actions 是一个纯 javascript 对象,它们必须有一个 type 属性表明正在执行的 action 的类型。实质上,action 是将数据从应用程序发送到 store 的有效载荷

reducer

一个 reducer 是一个纯函数,该函数以先前的 state 和一个 action 作为参数,并返回下一个 state

Redux Thunk 的作用是什么

Redux thunk 是一个允许你编写返回一个函数而不是一个 action 的 actions creators 的中间件。如果满足某个条件,thunk 则可以用来延迟 action 的派发(dispatch),这可以处理异步 action 的派发(dispatch)

何为纯函数(pure function)

一个纯函数是一个不依赖于且不改变其作用域之外的变量状态的函数,这也意味着一个纯函数对于同样的参数总是返回同样的结果

combineReducers

combineReducers 函数主要用来接收一个对象,将参数过滤后返回一个函数。该函数里有一个过滤参数后的对象 finalReducers,遍历该对象,然后执行对象中的每一个 reducer 函数,最后将新的 state 返回

介绍Redux数据流的流程

view 到 redux 的过程中会派发一个 action , action 通过 Store 的 dispatch 方法,会派发给 store , store接收到 action ,再连同之前的 state 一起传给 reducer , reducer 返回一个新的数据给 store , store 就可以去改变自己的 state ,组件接收到新的 state 就可以重新渲染页面了

其中,redux有三个基本属性,Action,Reducer,Store

  • Action 是把数据从应用传到 store 的有效载体,也是 store 数据的唯一来源,用法是通过 store.dispatch() 把 action 传到 store。(Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。)
  • 这是 reducer 要做的事情。reducer 负责管理整个业务里边的数据,包括处理数据,存储数据等。刚刚说了store.dispatch() 会把 action 传到 store ,但是store并不知道怎么处理这个数据,需要去 reducer 进行查找,所以需要把当前 store 里存在的数据和接收的 action 转给 reducer , reducer 处理好了之后再转给 store。reducer 返回的必须是一个函数,这个函数里面接收两个参数,一个是 state ,另一个是 action。
  • 既然可以使用 action 来描述“发生了什么”,使用 reducers 来根据 action 更新 state 的用法。那Store 就是把它们联系到一起的对象,它负责存储项目应用中的所有数据,通过redux的createStore()方法创建store公共数据存储区域,然后把 reducer 传给 store,就可以在 reducer 中查看数据并做处理,const store = createStore(reducer)
    Store提供了一些方法。让我们很方便的操作数据:
  1. 维持应用的 state
  2. 提供 getState() 方法获取 store 的数据内容state
  3. 提供 dispatch(action) 方法派发 action ,更新 state
  4. 通过 subscribe(listener) 注册监听器,订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行

Redux如何实现多个组件之间的通信,多个组件使用相同状态如何进行管理

react-redux(Provider 传入到最外层组件store 在需要用到的地方 用 connect 获取 (mapStateToProps, mapDispatchToProps) 在页面中引用)
类似发布订阅模式, 一个地方修改了这个值, 其他所有使用了这个相同状态的地方也会更改

const App = (
  <Provider store={store}>
    <TodoList />
  </Provider>
)
  • Provider 是 react-redux 的一个核心API,连接着 store , Provider 里边所有的组件,都有能力获取到 store 里边的内容
  • react-redux 的另一个核心方法叫做 connect ,接收三个参数,最后一个参数是连接的组件,前面两个是连接的规则,分别是 mapStateToProps 和mapDispatchToProps方法。mapStateToProps翻译为中文就是把 store 里的数据state映射到组件props这个位置,为组件的 props 的数据, mapDispatchToProps可以把store.dispatch 挂载到props上。因为想要改变 store 里的内容,就要调用 dispatch 方法, dispatch 方法被映射到了 props 上,所以就可以通过 this.props.dispatch 方法去调用了。
  • 当我们在页面进行操作时,会执行某些事件方法,比如增删改查,会改变数据的状态,todolist 组件恰好通过 connect 跟数据做了连接,所以这块就变成了自动的流程,数据一旦发生改变,这个组件自动就会跟的变化。以前还需要 store.subscribe 做订阅,现在连订阅都可以不用了,页面自动响应数据发生变化
  • connect 方法可以这样理解,当你用 connect 把这个 UI 组件和一些数据和逻辑相结合时,返回的内容实际就是一个容器组件了,容器组件可以理解成数据处理包括派发这样的业务逻辑。所以 export default 导出的内容就是 connect 方法执行的结果,是一个容器组件
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

多个组件之间如何拆分各自的state,每块小的组件有自己的状态,它们之间还有一些公共的状态需要维护,如何思考这块

在实际项目开发中,如果 reducer 存放过多的数据,可能会造成代码的不可维护,我们需要把总的 reducer 拆分为多个子的 reducer,然后再做一个整合。
一个全局的 reducer , 页面级别的 reducer , 然后redux 里有个 combineReducers 把所有的 reducer 合在一起,小组件的使用与全局的使用是分开的互不影响

import { combineReducers } from 'redux'
import headerReducer from '../common/header/store/reducer'

export default combineReducers({
  header: headerReducer
})

然后在页面中通过state.header.focused访问

使用过的Redux中间件

react-redux
React-Redux是 redux官方提供的 React 绑定库,可以在 react 中非常方便是使用 redux
React-redux有两个核心方法。一个是Provider,另一个是connect

redux-thunk(把action 返回的对象 换成一个异步函数) :

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
) 
export default store

因为reducer 必须是纯函数,是不能进行ajax这种异步操作的,而在组件中直接进行ajax异步操作或者复杂的业务逻辑处理,组件会显得过于臃肿。
有了redux-thunk,就可以将ajax异步请求或者是复杂的逻辑放到 action 去处理,原则上 action 返回的是一个对象,但当我们使用 redux-thunk 中间件后, action 就可以返回一个函数了,继而可以在函数里边进行异步操作,也就可以把组件中获取数据的请求放入这个函数中了

export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/list.json').then((res) => {
      const data = res.data
      const action = initListAction(data)
      dispatch(action)
    })
  }
}

componentDidMount() {
  const action = getTodoList()
  store.dispatch(action) // 调用 store.dispatch()这个函数时,action这个函数就会被执行
}

redux-saga
虽然redux-thunk可以接受函数作为action,但函数的内部如果有多个异步操作,就需要为每一个异步操作都定义一个action,异步操作太为分散而不易维护
这个时候就可以用到redux-saga。 redux-saga也是做异步代码拆分的,可以完全替代 redux-thunk 。
要通过redux-saga的createSagaMiddleware()方法创建saga中间件,还需要有一个sagajs文件,这样通过sagaMiddleware.run(mySaga)来运行这个sagas文件,这样当组件dispatch一个action的时候,不仅reducer可以接收到,sagas文件中也可以接收到了

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'
import mySaga from './sagas'

const sagaMiddleware = createSagaMiddleware() // 创建saga中间件
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
) 
sagaMiddleware.run(mySaga)
export default store

sagajs导出的是个生成器函数,通过在生成器函数中使用takeEvery方法,捕捉派发出来的所有 action。takeEvery有两个参数,第一个参数时action type,第二个参数是函数,一旦接收到符合条件的 action type,就执行第二个参数方法,所以就可以把异步逻辑写到这个方法里集中处理了,这个方法中有一个put方法也可以派发action操作。类似于 redux 原始的 dispatch,都可以发出 action,且发出的 action 都会被 reducer 监听到

import { takeEvery, put  } from 'redux-saga/effects'
import axios from 'axios'

function* getInitList() {
  const res = yield axios.get('/list.json')
  const action = initListAction(res.data)
  yield put(action)
}
function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList)
}
export default mySaga

redux-saga 远比 redux-thunk 复杂的多, redux-saga 里边有非常多的api,我们只用了 takeEvery 、 put ,官方文档中还有很多我们经常用到的 call 、 takeLatest 等,在处理大型项目时, redux-saga 是要优于 redux-thunk 的;但是从另一角度来说, redux-thunk 几乎没有任何 api ,特点就是在 action 里面返回的内容不仅仅是个对象,还可以是个函数

只要使用了中间件,就需要使用applyMiddleware()函数,顾名思义,是运行中间件的函数。它是Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行。
中间件就是指的 dispatch 方法的一个封装或者升级。(redux-thunk中间件:对 dispatch 方法做了封装之后,既可以接收对象,又可以接收函数了)所以中间件本质上只是拓展了store.dispatch方法